<!doctype html><html lang=zh-cn><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>简明 x86 汇编指南</title><link rel=stylesheet href=https://note-2019-images.oss-cn-hangzhou.aliyuncs.com/notes.css media=all><script src=/static/main.js></script><body data-category=default data-clipid=1573525793><div id=toc_c>三</div><div id=toc></div><div class="mx-wc-main yue"><div><div><div><h3>译者序</h3><p>Translated from <a href=http://www.cs.virginia.edu/~evans/cs216/guides/x86.html>CS216, University of Virginia</a>.<p>一份非常好的 x86 汇编教程，国外 CS 课程所用资料，篇幅简短，逻辑清晰，合适作为入
门参考。以原理为主，有两个例子帮助理解。其开始提到使用 MicroSoft MASM 和 Visual
Studio，但非必须, 事实上如果你有 Linux 更好。<p><strong>本文根据原文内容意译，而非逐词逐句翻译，如需了解更多，推荐阅读</strong><a href=http://www.cs.virginia.edu/~evans/cs216/guides/x86.html>原文
</a>.<hr><p>内容：<strong>寄存器, 内存和寻址, 指令, 函数调用约定（Calling Convention）</strong><p>本文介绍 <strong>32bit x86 汇编</strong>基础，覆盖其中虽小但很有用的一部分。
有多种汇编语言可以生成 x86 机器代码。我们在 CS216 课程中使用的是 MASM（
Microsoft Macro Assembler）。MASM 使用标准 Intel 语法。<p>整套 x86 指令集庞大而复杂（Intel x86 指令集手册超过 2900 页），本文不会全部覆盖。<h2>1. 参考资料</h2><ul><li>Guide to Using Assembly in Visual Studio — a tutorial on building and debugging assembly code in Visual Studio<li>Intel x86 Instruction Set Reference<li>Intel’s Pentium Manuals (the full gory details)</ul><h2>2. 寄存器</h2><p><img src=https://note-2019-images.oss-cn-hangzhou.aliyuncs.com/7b48db11.png width=50% height=50% data-url=https://arthurchiao.github.io/assets/img/x86-asm-guide/x86-registers.png data-uploaded=true><p>Fig 2.1 x86 registers<p>现代 x86 处理器有 8 个 32 bit 寄存器，如图 1 所示。寄存器名字是早期计算机历史上
流传下来的。例如，EAX 表示 Accumulator，因为它用作算术运算的累加器，ECX 表示
Counter，用来存储循环变量（计数）。大部分寄存器的名字已经失去了原来的意义，但有
两个是例外：栈指针寄存器（Stack Pointer）ESP 和基址寄存器（ Base Pointer）EBP。<p>对于 <code class="language-plaintext highlighter-rouge">EAX</code>, <code class="language-plaintext highlighter-rouge">EBX</code>, <code class="language-plaintext highlighter-rouge">ECX</code>, <code class="language-plaintext highlighter-rouge">EDX</code> 四个寄存器，可以再将 32bit 划分成多个子寄存器，
每个子寄存器有专门的名字。例如 <code class="language-plaintext highlighter-rouge">EAX</code> 的高 16bit 叫 <code class="language-plaintext highlighter-rouge">AX</code>（去掉 E, E 大概表示
<strong>Extended</strong>）,低 8bit 叫 <code class="language-plaintext highlighter-rouge">AL</code> (<strong>Low</strong>）, 8-16bit 叫 <code class="language-plaintext highlighter-rouge">AH</code> （<strong>High</strong>）。如图 1
所示。<p>在汇编语言中，这些寄存器的名字是<strong>大小写无关</strong>的，既可以用 <code class="language-plaintext highlighter-rouge">EAX</code>，也可以写 <code class="language-plaintext highlighter-rouge">eax</code>。<h2>3. 内存和寻址模式</h2><h3>3.1 声明静态数据区</h3><p><code class="language-plaintext highlighter-rouge">.DATA</code> 声明静态数据区。<p>数据类型修饰原语：<ul><li><code class="language-plaintext highlighter-rouge">DB</code>: Byte, 1 Byte（<code class="language-plaintext highlighter-rouge">DB</code> 的 <code class="language-plaintext highlighter-rouge">D</code> 可能表示 Data）<li><code class="language-plaintext highlighter-rouge">DW</code>: Word, 2 Bytes<li><code class="language-plaintext highlighter-rouge">DD</code>: Double Word, 4 Bytes</ul><p>例子：<div><div><pre class=highlight><code>.DATA
var     DB 64    ; 声明一个 byte 值, referred to as location var, containing the value 64.
var2    DB ?     ; 声明一个未初始化 byte 值, referred to as location var2.
        DB 10    ; 声明一个没有 label 的 byte 值, containing the value 10. Its location is var2 + 1.
X       DW ?     ; 声明一个 2-byte 未初始化值, referred to as location X.
Y       DD 30000 ; 声明一个 4-byte 值, referred to as location Y, initialized to 30000.
</code></pre></div></div><p>和高级语言不同，<strong>在汇编中只有一维数组</strong>，只有没有二维和多维数组。一维数组其实就
是内存中的一块连续区域。另外，<code class="language-plaintext highlighter-rouge">DUP</code> 和字符串常量也是声明数组的两种方法。<p>例子：<div><div><pre class=highlight><code>Z       DD 1, 2, 3      ; 声明 3 个 4-byte values, 初始化为 1, 2, and 3. The value of location Z + 8 will be 3.
bytes   DB 10 DUP(?)    ; 声明 10 个 uninitialized bytes starting at location bytes.
arr     DD 100 DUP(0)   ; 声明 100 个 4-byte words starting at location arr, all initialized to 0
str     DB &#39;hello&#39;,0    ; 声明 6 bytes starting at the address str, 初始化为 hello and the null (0) byte.
</code></pre></div></div><h3>3.2 内存寻址 (Addressing Memory)</h3><p>有多个指令可以用于内存寻址，我们先看使用 <code class="language-plaintext highlighter-rouge">MOV</code> 的例子。<code class="language-plaintext highlighter-rouge">MOV</code> 将在内存和寄存器之
间移动数据，接受两个参数：第一个参数是目的地，第二个是源。<p>合法寻址的例子：<div><div><pre class=highlight><code>mov eax, [ebx]        ; Move the 4 bytes in memory at the address contained in EBX into EAX
mov [var], ebx        ; Move the contents of EBX into the 4 bytes at memory address var. (Note, var is a 32-bit constant).
mov eax, [esi-4]      ; Move 4 bytes at memory address ESI + (-4) into EAX
mov [esi+eax], cl     ; Move the contents of CL into the byte at address ESI+EAX
mov edx, [esi+4*ebx]  ; Move the 4 bytes of data at address ESI+4*EBX into EDX
</code></pre></div></div><p><strong>非法寻址</strong>的例子：<div><div><pre class=highlight><code>mov eax, [ebx-ecx]      ; 只能对寄存器的值相加，不能相减
mov [eax+esi+edi], ebx  ; 最多只能有 2 个寄存器参与地址计算
</code></pre></div></div><h3>3.3 数据类型(大小)原语（Size Directives）</h3><p>修饰<strong>指针</strong>类型：<ul><li><code class="language-plaintext highlighter-rouge">BYTE PTR</code> - 1 Byte<li><code class="language-plaintext highlighter-rouge">WORD PTR</code> - 2 Bytes<li><code class="language-plaintext highlighter-rouge">DWORD PTR</code> - 4 Bytes</ul><div><div><pre class=highlight><code>mov BYTE PTR [ebx], 2   ; Move 2 into the single byte at the address stored in EBX.
mov WORD PTR [ebx], 2   ; Move the 16-bit integer representation of 2 into the 2 bytes starting at the address in EBX.
mov DWORD PTR [ebx], 2  ; Move the 32-bit integer representation of 2 into the 4 bytes starting at the address in EBX.
</code></pre></div></div><h2>4. 指令</h2><p>三大类：<ul><li>数据移动<ol><li><code class="language-plaintext highlighter-rouge">mov</code><li><code class="language-plaintext highlighter-rouge">push</code><li><code class="language-plaintext highlighter-rouge">pop</code><li><code class="language-plaintext highlighter-rouge">lea</code> - Load Effective Address</ol><li>算术/逻辑运算<ol><li><code class="language-plaintext highlighter-rouge">add</code>, <code class="language-plaintext highlighter-rouge">sub</code><li><code class="language-plaintext highlighter-rouge">inc</code>, <code class="language-plaintext highlighter-rouge">dec</code><li><code class="language-plaintext highlighter-rouge">imul</code>, <code class="language-plaintext highlighter-rouge">idiv</code><li><code class="language-plaintext highlighter-rouge">and</code>, <code class="language-plaintext highlighter-rouge">or</code>, <code class="language-plaintext highlighter-rouge">xor</code><li><code class="language-plaintext highlighter-rouge">not</code><li><code class="language-plaintext highlighter-rouge">neg</code><li><code class="language-plaintext highlighter-rouge">shl</code>, <code class="language-plaintext highlighter-rouge">shr</code></ol><li>控制流<ol><li><code class="language-plaintext highlighter-rouge">jmp</code><li><code class="language-plaintext highlighter-rouge">je</code>, <code class="language-plaintext highlighter-rouge">jne</code>, <code class="language-plaintext highlighter-rouge">jz</code>, <code class="language-plaintext highlighter-rouge">jg</code>, <code class="language-plaintext highlighter-rouge">jl</code> …<li><code class="language-plaintext highlighter-rouge">cmp</code><li><code class="language-plaintext highlighter-rouge">call</code>, <code class="language-plaintext highlighter-rouge">ret</code></ol></ul><h2>5. 调用约定</h2><p><strong>这是最重要的部分。</strong><p>子过程（函数）调用需要遵守一套共同的<strong>调用约定</strong>（<strong><em>Calling Convention</em></strong>）。
<strong>调用约定是一个协议，规定了如何调用以及如何从过程返回</strong>。例如，给定一组 calling
convention rules，程序员无需查看子函数的定义就可以确定如何将参数传给它。进一步地
，给定一组 calling convention rules，高级语言编译器只要遵循这些 rules，就可以使
得汇编函数和高级语言函数互相调用。<p>Calling conventions 有多种。我们这里介绍使用最广泛的一种：<strong>C 语言调用约定</strong>（C
Language Calling Convention）。遵循这个约定，可以使汇编代码安全地被 C/C++ 调用
，也可以从汇编代码调用 C 函数库。<p>C 调用约定:<ul><li>强烈依赖<strong>硬件栈</strong>的支持 (hardwared-supported stack)<li>基于 <code class="language-plaintext highlighter-rouge">push</code>, <code class="language-plaintext highlighter-rouge">pop</code>, <code class="language-plaintext highlighter-rouge">call</code>, <code class="language-plaintext highlighter-rouge">ret</code> 指令<li>子过程<strong>参数通过栈传递</strong>: 寄存器保存在栈上，子过程用到的局部变量也放在栈上</ul><p>在大部分处理器上实现的大部分高级过程式语言，都使用与此相似的调用惯例。<p>调用惯例分为两部分。第一部分用于 <strong>调用方</strong>（<strong><em>caller</em></strong>），第二部分用于<strong>被调
用方</strong>（<strong><em>callee</em></strong>）。需要强调的是，错误地使用这些规则将导致<strong>栈被破坏</strong>，程序
很快出错；因此在你自己的子过程中实现 calling convention 时需要格外仔细。<p><img src=https://note-2019-images.oss-cn-hangzhou.aliyuncs.com/845a54a5.png width=60% height=60% data-url=https://arthurchiao.github.io/assets/img/x86-asm-guide/stack-convention.png data-uploaded=true><p>Fig 5.1 Stack during Subroutine Call<h3>5.1 调用方规则 (Caller Rules)</h3><p>在一个子过程调用之前，调用方应该：<ol><li><p><strong>保存应由调用方保存的寄存器</strong>（<strong><em>caller-saved</em></strong> registers): <code class="language-plaintext highlighter-rouge">EAX</code>, <code class="language-plaintext highlighter-rouge">ECX</code>,
<code class="language-plaintext highlighter-rouge">EDX</code><p>这几个寄存器可能会被被调用方（callee）修改，所以先保存它们，以便调用结
束后恢复栈的状态。<li><p><strong>将需要传给子过程的参数入栈</strong>（push onto stack)<p>参数按<strong>逆序</strong> push 入栈（最后一个参数先入栈）。由于栈是向下生长的，第一个参数
会被存储在最低地址（<strong>这个特性使得变长参数列表成为可能</strong>）。<li><p><strong>使用 <code class="language-plaintext highlighter-rouge">call</code> 指令，调用子过程(函数）</strong><p><code class="language-plaintext highlighter-rouge">call</code> 先将返回地址 push 到栈上，然后开始执行子过程代码。子过程代码需要遵
守的 callee rules。</ol><p>子过程返回后（<code class="language-plaintext highlighter-rouge">call</code> 执行结束之后），被调用方会将返回值放到 <code class="language-plaintext highlighter-rouge">EAX</code> 寄存器，调用方
可以从中读取。为恢复机器状态，调用方需要做：<ol><li><p><strong>从栈上删除传递的参数</strong><p>栈恢复到准备发起调用之前的状态。<li><p><strong>恢复由调用方保存的寄存器</strong>（<code class="language-plaintext highlighter-rouge">EAX</code>, <code class="language-plaintext highlighter-rouge">ECX</code>, <code class="language-plaintext highlighter-rouge">EDX</code>）—— 从栈上 pop 出来<p>调用方可以认为，除这三个之外，其他寄存器的值没有被修改过。</ol><h4>例子</h4><div><div><pre class=highlight><code>push [var] ; Push last parameter first
push 216   ; Push the second parameter
push eax   ; Push first parameter last

call _myFunc ; Call the function (assume C naming)

add esp, 12
</code></pre></div></div><h3>5.2 被调用方规则 (Callee Rules)</h3><ol><li><p><strong>将寄存器 <code class="language-plaintext highlighter-rouge">EBP</code> 的值入栈，然后 copy <code class="language-plaintext highlighter-rouge">ESP</code> to <code class="language-plaintext highlighter-rouge">EBP</code></strong><div><div><pre class=highlight><code>push ebp
mov  ebp, esp
</code></pre></div></div><li><p><strong>在栈上为局部变量分配空间</strong><p>栈自顶向下生长，故随着变量的分配，栈顶指针不断减小。<li><p><strong>保存应有被调用方保存（<code class="language-plaintext highlighter-rouge">callee-saved</code>）的寄存器</strong> —— 将他们压入栈。包括 <code class="language-plaintext highlighter-rouge">EBX</code>,
<code class="language-plaintext highlighter-rouge">EDI</code>, <code class="language-plaintext highlighter-rouge">ESI</code></ol><p>以上工作完成，就可以执行子过程的代码了。当子过程返回后，必须做以下工作：<ol><li><p><strong>将返回值保存在 <code class="language-plaintext highlighter-rouge">EAX</code></strong><li><p><strong>恢复应由被调用方保存的寄存器</strong>(<code class="language-plaintext highlighter-rouge">EDI</code>, <code class="language-plaintext highlighter-rouge">ESI</code>) —— 从栈上 pop 出来<li><p><strong>释放局部变量</strong><li><p><strong>恢复调用方 base pointer <code class="language-plaintext highlighter-rouge">EBP</code> —— 从栈上 pop 出来</strong><li><p><strong>最后，执行 <code class="language-plaintext highlighter-rouge">ret</code>，返回给调用方 (caller)</strong></ol><h4>例子</h4><div><div><pre class=highlight><code>.486
.MODEL FLAT
.CODE
PUBLIC _myFunc
_myFunc PROC
  ; Subroutine Prologue
  push ebp     ; Save the old base pointer value.
  mov ebp, esp ; Set the new base pointer value.
  sub esp, 4   ; Make room for one 4-byte local variable.
  push edi     ; Save the values of registers that the function
  push esi     ; will modify. This function uses EDI and ESI.
  ; (no need to save EBX, EBP, or ESP)

  ; Subroutine Body
  mov eax, [ebp+8]   ; Move value of parameter 1 into EAX
  mov esi, [ebp+12]  ; Move value of parameter 2 into ESI
  mov edi, [ebp+16]  ; Move value of parameter 3 into EDI

  mov [ebp-4], edi   ; Move EDI into the local variable
  add [ebp-4], esi   ; Add ESI into the local variable
  add eax, [ebp-4]   ; Add the contents of the local variable
                     ; into EAX (final result)

  ; Subroutine Epilogue 
  pop esi      ; Recover register values
  pop  edi
  mov esp, ebp ; Deallocate local variables
  pop ebp ; Restore the caller&#39;s base pointer value
  ret
_myFunc ENDP
END
</code></pre></div></div></div></div></div><hr><div><label>原网址: <a href=https://arthurchiao.github.io/blog/x86-asm-guide-trans-cn-zh/>访问</a></label><br><label>创建于: 2019-11-12 10:29:53</label><br><label>目录: default</label><br><label>标签: <code>dev</code></label></div></div><script>var info = {"clipId":"1573525793","format":"html","title":"简明 x86 汇编指南","link":"https://arthurchiao.github.io/blog/x86-asm-guide-trans-cn-zh/","category":"default","tags":["dev"],"created_at":"2019-11-12 10:29:53","filename":"index.html"};</script><script src=https://note-2019-images.oss-cn-hangzhou.aliyuncs.com/highlight.pack.js></script><link rel=stylesheet href=https://note-2019-images.oss-cn-hangzhou.aliyuncs.com/highlight.vs.css><script src=https://note-2019-images.oss-cn-hangzhou.aliyuncs.com/tocbot.min.js></script><script src=https://note-2019-images.oss-cn-hangzhou.aliyuncs.com/notes.js></script>